﻿using System.Net.Sockets;
using System;
using System.Net;
using System.Text;

namespace Sockets
{
    class ClientSocket
    {
        #region Member Variables
        private Socket client;
        private bool connected = false;
        private string remoteIP = "";
        private int remotePort = -1;
        #endregion

        #region Properties
        /// <summary>
        /// True if the <c>ClientSocket</c> is connected to a remote host, false if not connected
        /// </summary>
        public bool Connected
        {
            get { return connected; }
        }

        /// <summary>
        /// Gets or sets the IP of the remote host
        /// </summary>
        public string RemoteIP 
        {
            get
            {
                return remoteIP;
            }
            set
            {
                if (!connected)
                    remoteIP = value;
                else
                    throw new Exception("Cannot set the remoteIP when the ClientSocket is connected");
            }
        }

        /// <summary>
        /// Gets or sets the port of the remote host
        /// </summary>
        public int RemotePort
        {
            get
            {
                return remotePort;
            }
            set
            {
                if (!connected)
                    remotePort = value;
                else
                    throw new Exception("Cannot set the remotePort when the ClientSocket is connected");
            }
        }

        #endregion

        #region Events
        public delegate void dataReceivedHandler(object sender, DataReceivedEventArgs e);

        /// <summary>
        /// Fired if the attempt to connect is successful
        /// </summary>
        public event EventHandler OnConnectSuccess;

        /// <summary>
        /// Fired if the attempt to connect times out
        /// </summary>
        public event EventHandler OnConnectFailed;

        /// <summary>
        /// Fired if the <c>ClientSocket</c> gets disconnected from the remote host for any reason
        /// other than a manual disconnect.
        /// </summary>
        public event EventHandler OnDisconnect;

        /// <summary>
        /// Fired when a complete message has been received from the remote host
        /// </summary>
        public event dataReceivedHandler OnDataReceived;
        #endregion

        /// <summary>
        /// Connects to the remote host
        /// </summary>
        public void Connect()
        {
            if (connected)
                throw new Exception("The ClientSocket is already connected");
            else if (remotePort == -1)
                throw new Exception("Remote port not set");
            else if (remoteIP == "")
                throw new Exception("Remote IP not set");

            // It is assumed input validation has been done outside of this class, so none is preformed here
            try
            {
                IPHostEntry ipHostInfo = Dns.Resolve(remoteIP); // Deprecated.  I'm still using it... screw IPv6.
                IPAddress ipAddress = ipHostInfo.AddressList[0];
                IPEndPoint remoteEP = new IPEndPoint(ipAddress, remotePort);
                client = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);

                client.BeginConnect(remoteEP,
                    new AsyncCallback(ConnectCallback), client);
            }
            catch (Exception)
            {
                throw;
            }
        }

        private void ConnectCallback(IAsyncResult ar)
        {
            try
            {
                // Retrieve the socket from the state object.
                Socket theClient = (Socket)ar.AsyncState;

                // Complete the connection.
                theClient.EndConnect(ar);

                StateObject state = new StateObject();
                state.workSocket = client;
                connected = true;

                // Emit an event letting anything listening know that a connection was made successfully
                if (OnConnectSuccess != null)
                    OnConnectSuccess(this, new EventArgs());

                client.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
                    new AsyncCallback(ReceiveCallback), state);
            }
            catch (Exception)
            {
                // Failed to connect
                // Emit an event letting anything listening know that a connection was not made successfully                
                if(OnConnectFailed != null)
                    OnConnectFailed(this, new EventArgs());
            }
        }

        private void ReceiveCallback(IAsyncResult ar)
        {
            string content = String.Empty;
            int bytesRead = 0;

            try
            {
                // Retrieve the state object and the client socket 
                // from the asynchronous state object.
                StateObject state = (StateObject)ar.AsyncState;
                Socket theClient = state.workSocket;

                // Read data from the remote device
                if (client.Connected)
                    bytesRead = theClient.EndReceive(ar);
                else
                    return;

                if (bytesRead > 0)
                {
                    // There might be more data, so store the data received so far.
                    state.sb.Append(Encoding.ASCII.GetString(state.buffer, 0, bytesRead));
                    content = state.sb.ToString();

                    if (content.IndexOf("<EOF>") > -1)
                    {
                        content = content.Remove(content.Length - 5, 5);
                        state.sb.Remove(0, state.sb.Length);            
           
                        // All the data has been read from the client
                        if(OnDataReceived != null)
                            OnDataReceived(this, new DataReceivedEventArgs(content));
                    }

                    theClient.BeginReceive(state.buffer, 0, StateObject.BufferSize, 0,
                        new AsyncCallback(ReceiveCallback), state);
                }
                else
                {
                    // Server disconcected
                    connected = false;
                    client.Shutdown(SocketShutdown.Both);
                    client.Close();
                    
                    if(OnDisconnect != null)
                        OnDisconnect(this, new EventArgs());
                }
            }
            catch (Exception)
            {
                // Server disconnected
                connected = false;
                if(OnDisconnect != null)
                    OnDisconnect(this, new EventArgs());
            }
        }

        public void Send(string message)
        {
            // Convert the string data to byte data using ASCII encoding.
            byte[] byteData = Encoding.ASCII.GetBytes(message + "<EOF>");

            // Begin sending the data to the remote device.
            client.BeginSend(byteData, 0, byteData.Length, 0,
                new AsyncCallback(SendCallback), client);
        }

        private void SendCallback(IAsyncResult ar)
        {
            try
            {
                // Retrieve the socket from the state object.
                Socket client = (Socket)ar.AsyncState;

                // Complete sending the data to the remote device.
                client.EndSend(ar);
            }
            catch (Exception e)
            {
                throw new Exception(e.ToString());
            }
        }

        /// <summary>
        /// Disconnect from the remote host
        /// </summary>
        public void Disconnect()
        {
            connected = false;
            client.Shutdown(SocketShutdown.Both);
            client.Close();
        }
    }

    /// <summary>
    /// Used for passing a parameter to subscribers
    /// </summary>
    public class DataReceivedEventArgs : EventArgs
    {
        public DataReceivedEventArgs(string data)
        {
            Data = data;
        }

        public string Data { get; set; }
    }
}
